import Konva from "konva";
import { Draw } from "../Draw/Draw";
import { Doc, Map, YMapEvent } from "yjs";
import { getRandomColor } from "../Utils";
import { IKeyMap } from "../Interface/Pptx";
import { WebsocketProvider } from "y-websocket";
import { updateShapeOptions } from "../Utils/konva";
import { rectCornerRadius } from "../Event/handle/konva/stage-mousedown";
import { IYjsWebsocket, YjsMapData, YjsSendData } from "../Interface/Websocket";

/**
 * websocket 协同管理器
 *  该模块仅与 draw 进行通信：
 *    1. 用户接受到其他用户的广播信息，通过 draw 的 onYjsEvent 实现事件回调
 *    2. 当前用户想要广播数据给其他用户，调用 draw.broadcast() 接口
 */

export class YjsWebsocket {
  private doc: Doc;
  private MAXRETRY = 3;
  private retry = 0;
  private timer = -1;
  private draw: Draw;
  private userid: string;
  private baseURL: string;
  private username: string;
  private usercolor: string;
  private connectState = false;
  private ymap!: Map<YjsMapData>; // map 存储的数据结构就是 [{key,value:YjsMapData}] ** 一定注意 Map 的delete 不然会内存溢出哈 **
  private provider!: WebsocketProvider;

  constructor(payload: IYjsWebsocket, draw: Draw) {
    this.draw = draw;

    // A Yjs document holds the shared data
    this.doc = new Doc();

    this.userid = payload.userid;

    // 处理用户名
    this.username = payload.username || `User_${this.doc.clientID}`;

    // 处理用户颜色
    this.usercolor = getRandomColor();

    this.baseURL = payload.url;

    // 目前暂时直接调用哈
    this.connect();
  }

  /**
   * 应该提供初始化方法，只有用户分享协同，进入文档，才执行协同
   *  1. 但是该功能需要用户系统，提供 shearID 才能唯一定位协同文档，需要提供 fileID
   */
  public connect() {
    // 必须要求同一篇文档内，如果不同的 roomname，则无法协同,此处应该是唯一的 fileID
    const roomname = "room_demo";

    this.ymap = this.doc.getMap(roomname);

    //  websocket 方式实现协同 服务在 /server/server.js 可通过 npm run server 命令开启服务
    const url =
      this.baseURL + "?client_type=unipptx&client_userid" + this.userid;

    this.provider = new WebsocketProvider(url, roomname, this.doc);

    this.provider.on("status", (event: { status: string }) => {
      this.connectState = event.status === "connected";
      if (this.connectState) this.send({ type: `${this.userid}_join` });
      // 广播其他用户，'我'进入协同
      else this.reconnection(); // 重连机制
    });

    // 监听当前连接关闭事件
    this.provider.on("connection-close", this.close.bind(this));

    // 任何的事件，都直接触发 draw eventHandle 即可
    this.ymap.observe((event, transaction) => {
      if (transaction.local) return; // 本地触发的 observer 不需要观察
      this.onYjsEvent(event);
    });
  }

  // ** 供 draw 发送协同消息
  public send(payload: YjsSendData) {
    if (!this.connectState || !this.ymap) return;

    // 每一次发送消息之前，都要将当前用户的 Awareness 意识带上（用户的唯一标识）
    const data = Object.assign(payload, {
      userid: this.userid,
      username: this.username,
      usercolor: this.usercolor,
      timestamp: Date.now(),
    });

    /**
     * 然后存储到 ymap 中，即时会同步到其他客户端，因为监听了 ymap.on('observe')
     *  同时注意：key的唯一性，保证了map的记录不会重复，如果相同的key，则map保存的是最后一次操作记录
     *  例如：
     *    属性协同，用的 key 是 'shape_updateOptions'，
     *    用户A：设置颜色为 blue
     *    用户B：设置边框为 yellow
     *    用户C：设置宽度为 100
     *    那么，此时，map 中，key 为 'shape_updateOptions' 的值，为：width：100
     *    后续用户登录查看的 shape 属性，为：width：100，其他属性会丢失！
     *  因此，需要将用户A、B、C 的操作，合并成一个操作，再广播给其他用户/或者，将用户的操作细分为多个操作，不被覆盖亦可。
     *  但是，也会有一个问题，分为多个操作后，会导致 ymap 的堆栈溢出，因此，解决方案为：
     *    ！这个是比较合理的方法：需要合并操作，减少广播次数。
     *    及时delete key，避免堆栈溢出。
     */
    this.ymap.set(payload.type, data);
  }

  public getValue(key: string): YjsMapData | undefined {
    if (!this.connectState || !this.ymap) return;
    return this.ymap.get(key);
  }

  public deleteValue(key: string) {
    if (!this.connectState || !this.ymap) return;
    this.ymap.delete(key);
  }

  public clearMap() {
    if (!this.connectState || !this.ymap) return;
    this.ymap.clear();
  }

  /** 提供关闭方法 */
  public close() {
    if (!this.connectState) return;
    // 广播其他用户离开
    this.send({ type: `${this.userid}_leave` });
    this.provider.disconnect();
  }

  /** 重连机制 */
  private reconnection() {
    const message = this.draw.getMessage();
    // 不然断开链接，就重新连接
    if (this.retry < this.MAXRETRY) {
      this.timer = setTimeout(() => {
        console.warn("连接 Yjs 服务失败，正在尝试重新连接...");
        window.clearTimeout(this.timer);
        this.provider.connect();
        this.retry++;
      }, 2000) as unknown as number;
    } else {
      message.error("协同服务连接失败，请检查服务是否正常.");
      console.error("协同服务连接失败，请检查服务是否正常");
      this.provider.disconnect(); // 强制断开连接
    }
  }

  /** ymap 事件回调 */
  private onYjsEvent(event: YMapEvent<YjsMapData>) {
    // 通过 YMapEvent 解析数据，针对不同的 key 做不同的处理
    const eventMap: IKeyMap<string, (data: YjsMapData) => void> = {
      join: this.joinHandle.bind(this),
      leave: this.leaveHandle.bind(this),
      filename: this.renameHandle.bind(this),
      newSlide: this.newSlideHandle.bind(this),
      insert: this.insertShapeHandle.bind(this),
      colorMatch: this.colorMatchHandle.bind(this),
      textUpdate: this.textUpdateHandle.bind(this),
      background: this.backgroundHandle.bind(this),
      deleteSlide: this.deleteSlideHandle.bind(this),
      deleteGraph: this.deleteGraphHandle.bind(this),
      groupDragEnd: this.groupDragEndHandle.bind(this),
      transformEnd: this.transformEndHandle.bind(this),
      updatePoints: this.updatePointsHandle.bind(this),
      updateOptions: this.updateOptionsHandle.bind(this),
    };

    /** 为了方便处理，这里约定：所有的 key 均以 xxx_xxx 的形式传递，第二个 xxx 具有实际意义 */
    event.changes.keys.forEach((_change, key) => {
      const realKey = key.split("_")[1];
      const value = this.getValue(key);
      if (!value) return;
      if (eventMap[realKey]) eventMap[realKey](value);
      else console.log("未处理事件", key);
    });
  }

  // 公共函数 - 解析当前传入的 LayerID ，获取 layer
  private getLayer(ID: string) {
    const layerManager = this.draw.getLayerManager();
    const currentLayer = this.draw.getLayer();
    const layer = layerManager.getLayerByID(ID);
    if (!currentLayer || !layer) return;
    if (currentLayer.id() === ID) return currentLayer;
    else return layer;
  }

  /**
   * 新用户加入协同
   */
  private joinHandle(data: YjsMapData) {
    console.log("新用户加入协同", data.userid);
  }

  /**
   * 用户离开协同
   */
  private leaveHandle(data: YjsMapData) {
    console.log("用户离开协同", data.userid);
    this.deleteValue(data.type); // 删除 leave 记录
    this.deleteValue(`${data.userid}_join`); // 删除 join 记录
  }

  /**
   * 新建幻灯片
   *  这里需要注意的是 currentIndex
   *  影响的是新建幻灯片的插入顺序、还有就是 switch 的顺序
   *  order 是 ‘我’ 当前的顺序，需要按照这个顺序进行排序，不然会有问题
   */
  private newSlideHandle({ payload }: YjsMapData) {
    const layerManager = this.draw.getLayerManager();
    const { layerID, order } = payload;
    //  必须保持一致的 layerID
    const layer = new Konva.Layer({ id: layerID });
    // 但是我不能跳转到最新的幻灯片，因为 ‘我’ 正在编辑...
    // 1. 记录当前我所在的 index
    const currentIndex = layerManager.getCurrentIndex();

    // 2. 设置当前 index
    layerManager.setCurrentIndex(order);

    this.draw.addSlideToEditor(layer);
    if (currentIndex !== -1) {
      // 重新跳转回去
      layerManager.switchLayer(currentIndex);
    }
  }

  /**
   * 删除幻灯片
   */
  private deleteSlideHandle({ payload }: YjsMapData) {
    const layerManager = this.draw.getLayerManager();
    const { idx, layerID } = payload;
    layerManager.deleteSlide(idx);
    this.deleteValue(`${layerID}_newSlide`); // 删除 new 记录
    this.deleteValue(`${layerID}_deleteSlide`); // 删除 delete 记录
  }

  /**
   * 文件重命名
   */
  private renameHandle({ payload }: YjsMapData) {
    const root = this.draw.getRootBox();
    const { filename } = payload;
    //  获取当前的 filename
    const filenameSelector = ".konva-root-topmenu-filename";
    const filenameInput = <HTMLElement>root.querySelector(filenameSelector);
    const oldText = filenameInput.innerText;
    if (oldText === filename) return;
    filenameInput.innerText = filename;
  }

  /**
   * 插入元素
   */
  private async insertShapeHandle({ payload }: YjsMapData) {
    const konvaGraph = this.draw.getKonvaGraph();

    // 普通元素参数
    const { width, height, x, y, insertShape, id, layerID } = payload;

    // Line 专属参数
    const { points, pointerLength, pointerWidth, tension, dash } = payload;

    // Image 专属参数
    const { source } = payload;

    // RichText 专属参数
    const { delta } = payload;

    let shape = <Konva.Group | Konva.Arrow | null>null;

    const layer = this.getLayer(layerID);
    const addShape = () => {
      if (!shape || !layer) return;
      layer.add(shape);
      this.draw.render({ patchHistory: false });
    };

    switch (insertShape) {
      // 矩形
      case "rect1":
      case "rect2":
      case "rect3":
      case "rect4":
        shape = konvaGraph.Rect({
          id,
          width,
          height,
          x,
          y,
          cornerRadius: rectCornerRadius(insertShape),
        });
        addShape();
        break;

      // 普通文本
      case "text1":
        shape = konvaGraph.Text({
          id,
          x,
          y,
          width: 100,
          height: 50,
          text: "请输入文字",
          fontSize: 16,
          fontFamily: "Arial",
          fill: "#000",
        });
        addShape();
        break;

      // 箭头
      case "arrow1":
      case "arrow2":
      case "arrow3":
      case "arrow4":
      case "arrow5":
      case "arrow6":
      case "arrow7":
      case "arrow8":
        shape = konvaGraph.Arrow({
          id,
          width,
          height,
          x,
          y,
          insertShape,
        });
        addShape();
        break;

      // 线段
      case "line":
      case "line-dash":
      case "line-arrow":
      case "line-arrow-dash":
      case "line-z":
      case "line-zz":
      case "line-w":
      case "line-ww":
        shape = konvaGraph.Line({
          points,
          id,
          x,
          y,
          pointerLength,
          pointerWidth,
          tension,
          dash,
        });
        addShape();
        break;

      // Image
      case "image":
      case "latex":
        shape = await konvaGraph.Image({ source, id });
        addShape();
        break;

      case "richText":
        shape = await konvaGraph.RichText({ source, id, delta });
        addShape();
        break;

      // Logo
      case "logo":
        break;

      //  五角星
      case "star":
        break;

      // 圆环
      case "ring":
        break;

      // 圆
      case "circle":
        break;

      // 椭圆
      case "ellipse":
        break;

      // 三角形
      case "triangle":
        break;

      // 多边形
      case "polygon":
        break;

      default:
        break;
    }
  }

  /**
   * 文本更新事件 - 指的是 group dbclick handle
   */
  private textUpdateHandle({ payload }: YjsMapData) {
    const { layerID, text, groupID } = payload;
    const layer = this.getLayer(layerID);
    const group = <Konva.Group>layer?.findOne(`#${groupID}`);
    if (!group) return;
    const textNode = <Konva.Text>group.findOne("Text")!;
    textNode.setAttrs({ text });
    this.draw.render({ patchHistory: false });
  }

  /**
   * groupDragEndHandle group 元素移动结束后，更正位置信息
   */
  private groupDragEndHandle({ payload }: YjsMapData) {
    const { layerID, x, y, groupID } = payload;
    const layer = this.getLayer(layerID);
    const group = <Konva.Group>layer?.findOne(`#${groupID}`);
    if (!group) return;
    group.setAttrs({ x, y });
    this.draw.render({ patchHistory: false });
  }

  /**
   * transformEndHandle 形变结束后，更正缩放旋转
   */
  private transformEndHandle({ payload }: YjsMapData) {
    const { layerID, groupID, scaleX, scaleY, rotation, x, y } = payload;
    const layer = this.getLayer(layerID);
    const group = <Konva.Group>layer?.findOne(`#${groupID}`);
    if (!group) return;

    // 这拿到的属性是 group.children[0] 的哈
    const shape = group.children[0];
    shape.setAttrs({
      scaleX,
      scaleY,
      rotation,
      x,
      y,
    });

    // 添加 realWidth
    const width = group.width() * scaleX;
    const height = group.height() * scaleY;
    group.setAttrs({ realWidth: width, realHeight: height });

    // 修正 Text
    const text = group.findOne("Text");
    if (!text) return;

    text.width(width);
    text.height(height);

    // 旋转过程中 x y 会跟着变，因此，直接增加 父元素相同的偏移量即可
    text.setAttrs({ rotation, x, y });

    this.draw.render({ patchHistory: false });
  }

  /**
   * 删除图形：deleteGraphHandle
   */
  private deleteGraphHandle({ payload }: YjsMapData) {
    // 解析 数据
    const { deletedIds, layerID } = payload;
    const layer = this.getLayer(layerID);
    if (!layer) return;
    (<string[]>deletedIds).forEach((id) => {
      layer.findOne(`#${id}`)?.destroy();
    });

    this.draw.render({ patchHistory: false });
  }

  /**
   * updatePointsHandle 线段控制点更新事件
   */
  private updatePointsHandle({ payload }: YjsMapData) {
    const { layerID, points, shapeID } = payload;
    const layer = this.getLayer(layerID);
    if (!layer) return;
    const line = <Konva.Line>layer.findOne(`#${shapeID}`);
    if (!line) return;
    line.points(points);
    this.draw.render({ patchHistory: false });
  }

  /**
   * 切换配色协同
   */
  private colorMatchHandle({ payload }: YjsMapData) {
    const colorScheme = this.draw.getColorScheme();
    colorScheme.setColorMatch(payload);
  }

  /**
   * 背景协同
   */
  private backgroundHandle({ payload }: YjsMapData) {
    const { fill, image, global } = payload;
    const layerManager = this.draw.getLayerManager();
    if (fill) layerManager.setBackgroundLayer({ fill, bgType: "fill" });
    if (image) layerManager.setBackgroundLayer({ image, bgType: "image" });
    if (global) layerManager.setBackgroundLayer({ global });
  }

  /**
   * 元素属性更新协同
   */
  private updateOptionsHandle({ payload }: YjsMapData) {
    const { options, layerID, shapeID } = payload;
    const layer = this.getLayer(layerID);
    if (!layer || !shapeID.length) return;

    // 先找到对应的元素 - 通过 id 循环遍历
    const selected = <Konva.Group[]>[];
    (<string[]>shapeID).forEach((id) => {
      const shape = <Konva.Group>layer.findOne(`#${id}`);
      if (shape) selected.push(shape);
    });

    // 调用 Utils 模块公共更新方法
    updateShapeOptions(selected, options);

    this.draw.render({ patchHistory: false });
  }
}
